#include <math.h>
#include "volume_buffer.h"

namespace venus {
VolumeBuffer::VolumeBuffer(GLint format, int width, int height, int depth, 
                           int banks)
    : width_(width),
      height_(height),
      depth_(depth),
      banks_(banks),
      blend_mode_(BLEND_NONE) {
  // create fbo
  fbo_ = new FramebufferObject();

  glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS_EXT, &max_banks_);
  if (banks_ > max_banks_) banks_ = max_banks_;
  // create textures
  textures_ = new GLuint [banks_];
  for(int i=0; i<banks_; i++) {
    textures_[i] = Create3dTexture(format, width_, height_, depth_);
  }
  // attach slice 0 of first texture to fbo for starters
  fbo_->Bind();
  fbo_->AttachTexture(GL_TEXTURE_3D, textures_[0], GL_COLOR_ATTACHMENT0_EXT,
                      0, 0);
  fbo_->IsValid();
  fbo_->Disable();
}

VolumeBuffer::~VolumeBuffer() {
  delete fbo_;
  for(int i=0; i<banks_; i++) {
    glDeleteTextures(1, &textures_[i]);
  }
  delete [] textures_;
}

GLuint VolumeBuffer::Create3dTexture(GLint internalformat, 
                                     int width, int height, int depth) {
  GLuint tex;
  glGenTextures(1, &tex);
  glBindTexture(GL_TEXTURE_3D, tex);
  glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  GLint mode = GL_CLAMP_TO_BORDER;
  glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, mode);
  glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, mode);
  glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, mode);
  glTexImage3D(GL_TEXTURE_3D, 0, internalformat, width, height, depth, 0,
               GL_RGBA, GL_FLOAT, 0);
  return tex;
}

void VolumeBuffer::SetWrapMode(GLint mode, int bank) {
  glBindTexture(GL_TEXTURE_3D, textures_[bank]);
  glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, mode);
  glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, mode);
  glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, mode);
}

void VolumeBuffer::SetFiltering(GLint mode, int bank) {
  glBindTexture(GL_TEXTURE_3D, textures_[bank]);
  glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, mode);
  glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, mode);
}

void VolumeBuffer::SetData(float *data, int bank) {
  glBindTexture(GL_TEXTURE_3D, textures_[bank]);
  glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, width_, height_, depth_, GL_RGBA,
                  GL_FLOAT, data);
}

// draw a slice of the volume
void VolumeBuffer::DrawSlice(float z) {
  glBegin(GL_QUADS);
  glTexCoord3f(0.0f, 0.0f, z); glVertex2f(-1.0f, -1.0f);
  glTexCoord3f(1.0f, 0.0f, z); glVertex2f(1.0f, -1.0f);
  glTexCoord3f(1.0f, 1.0f, z); glVertex2f(1.0f, 1.0f);
  glTexCoord3f(0.0f, 1.0f, z); glVertex2f(-1.0f, 1.0f);
  glEnd();
}

// run program on all slices of volume
void VolumeBuffer::RunProgram(ProgramCG *fprog, int bank) {
  fbo_->Bind();

  glMatrixMode(GL_MODELVIEW);
  glPushMatrix();
  glLoadIdentity();

  glMatrixMode(GL_PROJECTION);
  glPushMatrix();
  glLoadIdentity();
  glOrtho(-1.0, 1.0, -1.0, 1.0, -1.0, 1.0);

  glViewport(0, 0, width_, height_);
  glDisable(GL_DEPTH_TEST);

  switch(blend_mode_) {
  case BLEND_ADDITIVE:
    glEnable(GL_BLEND);
    glBlendFunc(GL_ONE, GL_ONE);
    break;
  }

  fprog->Activate();
  for(int z=0; z<depth_; z++) {
    // attach texture slice to FBO
    fbo_->AttachTexture(GL_TEXTURE_3D, textures_[bank],
                        GL_COLOR_ATTACHMENT0_EXT, 0, z);
    // render
    DrawSlice((z + 0.5f) / (float) depth_);
  }
  fprog->Deactivate();
  glDisable(GL_BLEND);

  glMatrixMode(GL_PROJECTION);
  glPopMatrix();
  glMatrixMode(GL_MODELVIEW);
  glPopMatrix();

  fbo_->Disable();
}
} // namespace venus
